package mbsl.commons.videos;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.htmlparser.Node;
import org.htmlparser.NodeFilter;
import org.htmlparser.Parser;
import org.htmlparser.tags.Div;
import org.htmlparser.tags.ImageTag;
import org.htmlparser.tags.LinkTag;
import org.htmlparser.tags.ParagraphTag;
import org.htmlparser.tags.TableRow;
import org.htmlparser.util.NodeList;
import org.htmlparser.util.ParserException;

import mbsl.extensions.MissingDependencyException;
import mbsl.util.DependencyChecker;

/**
 * A data miner for IMDB.
 * <p>
 * It caches all results, to avoid looking up things twice.  I've not investigated the legality of this tool.
 */
public class IMDBMiner {
	/**
	 * Creates a new miner.
	 * @throws MissingDependencyException IMDBMiner relies on the org.htmlparser library to do its internal parsing, and
	 * this exception will be thrown if that library is not on the classpath.
	 */
	public IMDBMiner() throws MissingDependencyException {
		if (!DependencyChecker.check("org.htmlparser.Parser"))
			throw new MissingDependencyException("IMDBMiner depends on the org.htmlparser library.");
		
		if (mDetailCache == null)
			mDetailCache = new HashMap<String, IMDBDetails>();
		if (mHitCache == null)
			mHitCache = new HashMap<String, List<IMDBHit>>();
		
		mHits = new ArrayList<IMDBHit>();
	}
	
	/**
	 * Starts a sweep for the passed string.
	 * @param pSearchString The string to search for.
	 * @return A list of {@link IMDBHit}s holding the details of the hits found, or null if something went wrong.
	 */
	@SuppressWarnings("serial")
	List<IMDBHit> search(String pSearchString) {
		if (pSearchString == null || pSearchString.length() < 3)
			return mHits;
		
		if (mHitCache.containsKey(pSearchString)) {
			mHits = mHitCache.get(pSearchString);
			
			return mHits;
		}

		try {
			Parser parser = new Parser(String.format(IMDB_SEARCH_URL, pSearchString));
			
			if (parser.getURL().startsWith("http://www.imdb.com/title/tt")) {
				mHits.add(new IMDBHit(null, null, null, parser.getURL()));

				mHitCache.put(pSearchString, mHits);
				
				return mHits;
			}
			
			// Extract the table tags with SUMMARY="Game Match" (which, conveniently, hold the matches)
			NodeList hitList = parser.parse(null).extractAllNodesThatMatch(new NodeFilter() {
				public boolean accept(Node pNode) {
					if (!ParagraphTag.class.isInstance(pNode))
						return false;
					
					ParagraphTag node = (ParagraphTag)pNode;
					
					return node.getStringText().startsWith("<b>Popular Titles</b>") || node.getStringText().startsWith("<b>Titles (Exact Matches)</b>") || node.getStringText().startsWith("<b>Titles (Partial Matches)</b>");
				}
			}, true);
			// For each of these, extract the hits
			for (int i = 0; i < hitList.size(); i++) {
				NodeList nodeList = hitList.elementAt(i).getChildren().extractAllNodesThatMatch(new NodeFilter() {
					public boolean accept(Node pNode) {
						return TableRow.class.isInstance(pNode);
					}
				}, true);
				
				String type = null;
				String nodeText = ((ParagraphTag)hitList.elementAt(i)).getStringText();
				
				if (nodeText.startsWith("<b>Popular Titles</b>")) {
					type = "Popular";
				} else if (nodeText.startsWith("<b>Titles (Exact Matches)</b>"))
					type = "Exact";
				else if (nodeText.startsWith("<b>Titles (Partial Matches)</b>"))
					type = "Partial";
				
				// For each hit, extract the details
				for (int j = 0; j < nodeList.size(); j++) {
					int offset = 0;
					
					Node node = nodeList.elementAt(j).getChildren().elementAt(3);

					if (ImageTag.class.isInstance(node.getChildren().elementAt(0)))
						offset = 2; // If the hit has an image attached, the information moves down two nodes.

					String title = node.getChildren().elementAt(0 + offset).getFirstChild().getText();
					String year = node.getChildren().elementAt(1 + offset).getText().substring(2, 6);
					String location = "http://www.imdb.com" + ((LinkTag)node.getChildren().elementAt(0 + offset)).getAttribute("href");

					IMDBHit hit = new IMDBHit(title, year, type, location);
					
					mHits.add(hit);
				}				
			}

			mHitCache.put(pSearchString, mHits);
			
			return mHits;
		} catch (ParserException pE) {
			return null;
		}
	}
	/**
	 * Retrieves the details about the hit at the passed index.
	 * @param pIndex The index of the hit in the list returned by {@link #search(String)}.
	 * @return The details about the hit.
	 */
	IMDBDetails get(int pIndex) {
		IMDBDetails details = get(mHits.get(pIndex).getLocation());
		
		if (details == null)
			details = get(mHits.get(pIndex).getLocation());
		
		return details;
	}
	/**
	 * Retrieves the details about the item at the passed URL.
	 * @param pLocation The URL to the item's main page at IMDB.
	 * @return The details about the item.
	 */
	@SuppressWarnings("serial")
	synchronized IMDBDetails get(String pLocation) {
		IMDBDetails details = mDetailCache.get(pLocation);
		
		if (details != null)
			return details;
		
		try {
			String name = null;
			String rating = null;
			List<String> directors = new ArrayList<String>();
			List<String> writers = new ArrayList<String>();
			String year = null;
			List<String> genres = new ArrayList<String>();
			NodeList nodes;
			
			Parser parser = new Parser(pLocation);
			// Retrieve the <div> that holds the information
			Node mainNode = parser.extractAllNodesThatMatch(new NodeFilter() {
				public boolean accept(Node pNode) {
					if (Div.class.isInstance(pNode)) {
						Div tag = (Div)pNode;
						
						String id = tag.getAttribute("id");
						
						if (id != null)
							return tag.getAttribute("id").equals("tn15main");
						else
							return false;
					} else
						return false;
				}
			}).elementAt(0);
			
			Node node = mainNode.getChildren().extractAllNodesThatMatch(new NodeFilter() {
				public boolean accept(Node pNode) {
					if (Div.class.isInstance(pNode)) {
						Div tag = (Div)pNode;
						
						String id = tag.getAttribute("id");
						
						if (id != null)
							return tag.getAttribute("id").equals("tn15title");
						else
							return false;
					} else
						return false;
				}
			}).elementAt(0);
			
			name = node.getChildren().elementAt(1).getChildren().elementAt(0).getText();
			year = node.getChildren().elementAt(1).getChildren().elementAt(1).getChildren().elementAt(1).getChildren().elementAt(0).getText();
			
			node = mainNode.getChildren().extractAllNodesThatMatch(new NodeFilter() {
				public boolean accept(Node pNode) {
					if (Div.class.isInstance(pNode)) {
						Div tag = (Div)pNode;
						
						String id = tag.getAttribute("id");
						
						if (id != null)
							return tag.getAttribute("id").equals("tn15rating");
						else
							return false;
					} else
						return false;
				}
			}, true).elementAt(0);
			
			if (node != null)
				rating = node.getChildren().elementAt(3).getChildren().elementAt(3).getChildren().elementAt(2).getText();
			
			node = mainNode.getChildren().extractAllNodesThatMatch(new NodeFilter() {
				public boolean accept(Node pNode) {
					if (Div.class.isInstance(pNode)) {
						Div tag = (Div)pNode;
						
						String id = tag.getAttribute("id");
						
						if (id != null)
							return tag.getAttribute("id").equals("director-info");
						else
							return false;
					} else
						return false;
				}
			}, true).elementAt(0);
			
			if (node != null) {
				nodes = node.getChildren().extractAllNodesThatMatch(new NodeFilter() {
					public boolean accept(Node pNode) {
						return LinkTag.class.isInstance(pNode) && !pNode.getChildren().elementAt(0).getText().equalsIgnoreCase("more");
					}
				});
				
				for (int i = 0; i < nodes.size(); i++)
					directors.add(nodes.elementAt(i).getChildren().elementAt(0).getText());
			}
			
			node = mainNode.getChildren().extractAllNodesThatMatch(new NodeFilter() {
				public boolean accept(Node pNode) {
					if (Div.class.isInstance(pNode)) {
						Div tag = (Div)pNode;
						
						String id = tag.getAttribute("class");
						
						if (id != null && id.equals("info") && tag.getChild(1) != null && tag.getChild(1).getChildren() != null) {
							return tag.getChild(1).getChildren().elementAt(0).getText().startsWith("Writer");
						}
						return false;
					} else
						return false;
				}
			}, true).elementAt(0);
			
			if (node != null) {
				nodes = node.getChildren().extractAllNodesThatMatch(new NodeFilter() {
					public boolean accept(Node pNode) {
						return LinkTag.class.isInstance(pNode) && !pNode.getChildren().elementAt(0).getText().equalsIgnoreCase("more");
					}
				});
				
				for (int i = 0; i < nodes.size(); i++)
					writers.add(nodes.elementAt(i).getChildren().elementAt(0).getText());
			}
			
			node = mainNode.getChildren().extractAllNodesThatMatch(new NodeFilter() {
				public boolean accept(Node pNode) {
					if (Div.class.isInstance(pNode)) {
						Div tag = (Div)pNode;
						
						String id = tag.getAttribute("class");
						
						if (id != null && id.equals("info") && tag.getChild(1) != null && tag.getChild(1).getChildren() != null) {
							return tag.getChild(1).getChildren().elementAt(0).getText().startsWith("Genre");
						}
						return false;
					} else
						return false;
				}
			}, true).elementAt(0);
			
			nodes = node.getChildren().extractAllNodesThatMatch(new NodeFilter() {
				public boolean accept(Node pNode) {
					return LinkTag.class.isInstance(pNode);
				}
			});
			
			for (int i = 0; i < nodes.size() - 1; i++)
				genres.add(nodes.elementAt(i).getChildren().elementAt(0).getText());
			
			IMDBDetails d = new IMDBDetails();
			
			d.setDirectors(directors);
			d.setGenres(genres);
			d.setName(name);
			d.setRating(rating);
			d.setWriters(writers);
			d.setYear(year);

			mDetailCache.put(pLocation, d);
			
			return d;
		} catch (ParserException pE) {
			return null;
		}
	}
	public static void main(String[] pArgs) {
		IMDBMiner miner = new IMDBMiner();
		
		for (IMDBHit k : miner.search("The Da Vinci Code"))
			System.out.println(k);
		
		
	}
	/**
	 * Details of an IMDB search hit, as found on an IMDB search page.
	 */
	class IMDBHit {
		private String mName;
		private String mYear;
		private String mType;
		private String mLocation;
		
		private IMDBHit(String pName, String pYear, String pType, String pLocation) {
			mName = pName;
			mYear = pYear;
			mType = pType;
			mLocation = pLocation;
		}
		/** The title of the hit. */
		String getName() {
			return decodeHTML(mName);
		}
		/** The year associated with the hit. */
		String getYear() {
			return decodeHTML(mYear);
		}
		/** The type of hit. */
		String getType() {
			return decodeHTML(mType);
		}
		/** The location of the hit, expressed as an URL. */
		String getLocation() {
			return mLocation;
		}
		public String toString() {
			return String.format("%s (%s, %s)", getName(), getYear(), getType());
		}
	}
	/**
	 * Details of an IMDB item, as found on an IMDB title page.
	 */
	class IMDBDetails {
		/** Returns a list of strings holding the names of the directors of the item. */
		List<String> getDirectors() {
			return mDirectors;
		}
		/** Sets the director list. */
		private void setDirectors(List<String> pDirectors) {
			mDirectors = pDirectors;
		}
		/** Returns a list of strings holding the names of the writers of the item. */
		List<String> getWriters() {
			return mWriters;
		}
		/** Sets the writer list. */
		private void setWriters(List<String> pWriters) {
			mWriters = pWriters;
		}
		/** Returns a list of strings holding the genres of the item. */
		List<String> getGenres() {
			return mGenres;
		}
		/** Sets the genre list. */
		private void setGenres(List<String> pGenres) {
			mGenres = pGenres;
		}
		/** Returns the full name of the item. */
		String getName() {
			return decodeHTML(mName);
		}
		/** Sets the full name. */
		private void setName(String pName) {
			mName = pName;
		}
		/** Returns the IMDB rating of the item. */
		Double getRating() {
			try {
				return Double.parseDouble(mRating.substring(0, mRating.indexOf('/')));
			} catch (Exception e) {
				return null;
			}
		}
		/** 
		 * Sets the rating.
		 * <p>
		 * Parsing will be done by the bean, so this should be the exact text found in the rating field. 
		 */
		private void setRating(String pRating) {
			mRating = pRating;
		}
		/** Returns the year associated with the item. */
		String getYear() {
			return mYear;
		}
		/** Sets the year. */
		private void setYear(String pYear) {
			mYear = pYear;
		}
		public String toString() {
			return String.format("Name: %s, Rating: %f, Date: %s", getName(), getRating(), getYear());
		}
		private String mName;
		private String mRating;
		private List<String> mDirectors;
		private List<String> mWriters;
		private String mYear;
		private List<String> mGenres;
	}
	/** Replaces some frequent HTML entities with their unicode equivalents. */
	private String decodeHTML(String pInput) {
		return pInput
			.trim()
			.replaceAll("&nbsp;", " ")
			.replaceAll("&#x26;", "&")
			.replaceAll("&#x27;", "'")
		;
	}
	/** The URL for the IMDB search, with "%s" where the search string should go. */
	private static final String IMDB_SEARCH_URL = "http://www.imdb.com/find?s=tt&q=%s";
	private List<IMDBHit> mHits;
	private static Map<String, IMDBDetails> mDetailCache;
	private static Map<String, List<IMDBHit>> mHitCache;
}
